﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

using Cookbook.Common;
using Cookbook.Services.Core.Commanding;
using Cookbook.Services.Core.Commands;
using Cookbook.Services.Core.Persistency;
using Cookbook.Services.Core.Persistency.NHibernate;

namespace Cookbook.Services.Core.Handlers
{
    public class AttributeMappedCommandHandler : IHandles<VersionedCommand>
    {
        private readonly Func<CookbookUnitOfWork> unitOfWorkFactory;

        public AttributeMappedCommandHandler(Func<CookbookUnitOfWork> unitOfWorkFactory)
        {
            this.unitOfWorkFactory = unitOfWorkFactory;
        }

        public void Handle(VersionedCommand command)
        {
            var mappingAttribute = command.GetType().FindAttribute<AppliesToAggregateRootAttribute>();
            if (mappingAttribute != null)
            {
                using (var uow = unitOfWorkFactory())
                {
                    var aggregateRoot = uow.Get(mappingAttribute.AggregateRootType, command.Id,
                        command.Version);

                    if (MapsToProperties(command))
                    {
                        MapToProperties(command, aggregateRoot);
                    }
                    else if (MapsToMethod(command))
                    {
                        MapToMethod(command, aggregateRoot);
                    }
                    else
                    {
                        throw new InvalidOperationException("Don't know how to handle command " + command.GetType().Name);
                    }
                }
            }
        }

        private static bool MapsToProperties(object command)
        {
            return command.GetType().HasAttribute<MapsToAggregateRootPropertiesAttribute>();
        }

        private static bool MapsToMethod(object command)
        {
            return command.GetType().HasAttribute<MapsToAggregateRootMethodAttribute>();
        }

        private void MapToProperties(Command command, object aggregateRoot)
        {
            IEnumerable<PropertyInfo> commandProperties = GetCommandProperties(command);

            Type aggregateRootType = aggregateRoot.GetType();

            foreach (var commandProperty in commandProperties)
            {
                var aggregateProperty = aggregateRootType.GetProperty(commandProperty.Name);
                if (aggregateProperty == null && commandProperty.Name.EndsWith(DatabaseConstants.IdentitySuffix))
                {
                    string commmandPropertyNameWithoutSuffix = commandProperty.Name.Substring(0,
                        commandProperty.Name.Length - DatabaseConstants.IdentitySuffix.Length);
                    aggregateProperty = aggregateRootType.GetProperty(commmandPropertyNameWithoutSuffix);
                }
                if (aggregateProperty == null)
                {
                    throw new InvalidOperationException(string.Format(
                        "Aggregate root {0} does not have a property named {1}.", aggregateRootType.Name,
                        commandProperty.Name));
                }

                object commandValue = GetPropertyValue(command, commandProperty, aggregateProperty.PropertyType);
                UnwrappedInvoke(() => aggregateProperty.SetValue(aggregateRoot, commandValue, null));
            }
        }

        private static IEnumerable<PropertyInfo> GetCommandProperties(object command)
        {
            return from pi in command.GetType().GetProperties()
                   where (pi.Name != DatabaseConstants.IdentityColumn) && (pi.Name != DatabaseConstants.VersionColumn)
                   select pi;
        }

        private void MapToMethod(object command, object aggregateRoot)
        {
            var mappingAttribute = command.GetType().FindAttribute<MapsToAggregateRootMethodAttribute>();
            MethodInfo methodInfo = mappingAttribute.GetMethod();

            var propertyValues = methodInfo.GetParameters().Select(p => GetMatchingPropertyValue(command, p)).ToArray();

            UnwrappedInvoke(() => methodInfo.Invoke(aggregateRoot, propertyValues));
        }

        private object GetMatchingPropertyValue(object command, ParameterInfo parameter)
        {
            PropertyInfo property = FindPropertyNamed(command, parameter.Name);
            if (property == null)
            {
                property = FindPropertyNamed(command, parameter.Name + DatabaseConstants.IdentitySuffix);
            }

            if (property == null)
            {
                throw new InvalidOperationException(string.Format(
                    "Command {0} does not have a property that can be mapped on parameter '{1}' of method {2}",
                    command.GetType().Name, parameter.Name, parameter.Member.Name));
            }

            return GetPropertyValue(command, property, parameter.ParameterType);
        }

        private static PropertyInfo FindPropertyNamed(object command, string name)
        {
            return
                command.GetType().GetProperties().SingleOrDefault(
                    pi => pi.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase));
        }

        private object GetPropertyValue(object command, PropertyInfo commandProperty, Type expectedType)
        {
            var value = commandProperty.GetValue(command, null);

            if (IsReferenceToAssociation(commandProperty) && (value != null))
            {
                using (var uow = unitOfWorkFactory())
                {
                    value = uow.Get(expectedType, (long) value);
                }
            }

            return value;
        }

        private static bool IsReferenceToAssociation(PropertyInfo commandProperty)
        {
            return commandProperty.Name.EndsWith(DatabaseConstants.IdentitySuffix);
        }

        private static void UnwrappedInvoke(Action action)
        {
            try
            {
                action();
            }
            catch (TargetInvocationException tie)
            {
                if (tie.InnerException != null)
                {
                    throw tie.InnerException;
                }
                else
                {
                    throw;
                }
            }
        }
    }
}